
import socketio
import collections
import traceback
from datetime import datetime
from select import select

AGENT_NAME = 'httplib/0.1'

_HTTP_VERSION = 1.1
_CONTENT_TYPE = 'Content-Type'
_CONTENT_LENGTH = 'Content-Length'
_CONTENT_ENCODING = 'Content-Encoding'
_TRANSFER_ENCODING = 'Transfer-Encoding'
_AUTHORIZATION = 'Authorization'
_CONNECTION = 'Connection'
_USER_AGENT = 'User-Agent'
_SERVER = 'Server'
_HOST = 'Host'
_DATE = 'Date'

class HttpError(ValueError):
	def __init__(self, status, reason):
		ValueError.__init__(self, reason)
		self.httpstatus = status
#		self.reason = reason
#	def create_response(self, contentmgr):
#		return contentmgr.create_response(self.status, self.reason, self.object)

class BaseMessage(tuple):
	__slots__ = ()
	def __new__(cls, _arg1, _arg2, version, headers, body):
		return tuple.__new__(cls, (_arg1, _arg2, version, headers, body))
	version = property(lambda self: self[2])
	headers = property(lambda self: self[3].iteritems())
	body = property(lambda self: self[4])
	def getheader(self, key):
		return self[3].get(key)

	@staticmethod
	def csl_split(val):
		return [item.strip() for item in val.split(',')] if val else []
	@staticmethod
	def csl_join(lst):
		return ', '.join(lst) if lst else None
	@staticmethod
	def argp_split(val):
		val = val.split(';')
		return val[0], BaseMessage.parse_parms(val[1:])
	@staticmethod
	def argp_join(arg, parms):
		return '; '.join([arg] + BaseMessage.format_parms(parms))
	@staticmethod
	def parse_parms(lst):
		lst = [item.partition('=') for item in lst]
		return {key.strip(): val.strip() or True for key, sep, val in lst}
	@staticmethod
	def format_parms(dct):
		return ['{}={}'.format(key, val) for key, val in dct.iteritems()] # if dct else []
	@staticmethod
	def parse_date(val):
		return datetime.strptime(val, '%a, %d %b %Y %H:%M:%S GMT')
	@staticmethod
	def format_date(dt):
		return datetime.strftime(dt, '%a, %d %b %Y %H:%M:%S GMT')

class Request(BaseMessage):
	__slots__ = ()
	method = property(lambda self: self[0])
	resource = property(lambda self: self[1])
	def __repr__(self):
		return '{} {} HTTP/{:.2}\n{}\n\n{:.1000}'.format(self.method, self.resource, self.version,
					'\n'.join(['{}: {}'.format(key, val) for key, val in self.headers]),
					self.body or '')

class Response(BaseMessage):
	__slots__ = ()
	status = property(lambda self: self[0])
	reason = property(lambda self: self[1])
	success = property(lambda self: 200 <= self.status <= 299)
	def __repr__(self):
		return 'HTTP/{:.2} {:03} {}\n{}\n\n{:.1000}'.format(self.version, self.status, self.reason,
					'\n'.join(['{}: {}'.format(key, val) for key, val in self.headers]),
					self.body or '')

class ContentManager(object):

	def __init__(self, marshallers=(), encoders=(), encode=False):
		self._marshallst = []
		self._marshalmap = {}
		for contype, objtype, dumps, loads in marshallers:
			if type(contype) is str:
				contype = (contype,)
			self._marshalmap.update(dict.fromkeys(contype, loads))
			self._marshallst.append((contype[0], objtype, dumps))
		self._encodermap = {enctype: (encode, decode) for enctype, encode, decode in encoders}
		self._encodermap['identity'] = (lambda x: x, lambda x: x)
		self._defenctype = encoders[0][0] if encode and encoders else None

	def create_request(self, method, resource, content):
		return self._create_message(Request, method, resource, content)

	def create_response(self, content=None, error=None):
		if error is None:
			status = 200 if content is not None else 204
			reason = 'OK'
		else:
			status = getattr(error, 'httpstatus', 500)
			reason = str(error)
			content = traceback.format_exc()
		return self._create_message(Response, status, reason, content)

	def _create_message(self, msgtype, _arg1, _arg2, content):
		headers = {}
		if content is None:
			return msgtype(_arg1, _arg2, _HTTP_VERSION, headers, None)
		for contype, objtype, dumps in self._marshallst: # if contype is None else (contype,):
#			objtype, dumps, loads = self._marshalmap[contype]
			if isinstance(content, objtype):
				typeparms = {}
				content = dumps(content, typeparms)
				headers[_CONTENT_TYPE] = msgtype.argp_join(contype, typeparms)
				break
		else:
			content = bytes(content)
#			raise HttpError(415, 'Unsupported %s' % _CONTENT_TYPE)
		if self._defenctype:
			encode, decode = self._encodermap[self._defenctype]
			content = encode(content)
			headers[_CONTENT_ENCODING] = self._defenctype
		headers[_CONTENT_LENGTH] = len(content)
		return msgtype(_arg1, _arg2, _HTTP_VERSION, headers, content)

	def process_message(self, message):
		content = message.body
		contype = message.getheader(_CONTENT_TYPE)
		enctype = message.getheader(_CONTENT_ENCODING)
		if content is None and type(message) is Request and message.method == 'GET':
				path, sep, parm = message.resource.partition('?')
				content = parm if sep else None
				contype = 'application/x-www-form-urlencoded'
		if content is None:
			return None
#		if not content and not contype:
#			return None
		if enctype:
#			if enctype not in self._encodermap:
#				raise HttpError(415, 'Unsupported %s' % _CONTENT_ENCODING)
			encode, decode = self._encodermap[enctype]
			content = decode(content)
#		if contype is None:
#			return content  # XXX fixme: what to do for a POST request if Content-Type isn't defined? Assume a single-parameter string? Or maybe assume application/x-www-form-urlencoded?
		if contype:
			contype, typeparms = message.argp_split(contype)
#			if contype not in self._marshalmap:
#				raise HttpError(415, 'Unsupported %s' % _CONTENT_TYPE)
			loads = self._marshalmap[contype]
			content = loads(content, typeparms)
		return content

	#def get_authentication(self, message):
	#	authent = message.getheader(_AUTHORIZATION)
	#	if authent is None:
	#		return None
	#	scheme, parms = authent.split(None, 1)
	#	if scheme != 'Basic':
	#		parms = message.parse_parms(message.csl_split(parms))
	#	if scheme not in self._authentmap:
	#		raise HttpError(403, 'Unsupported authentication scheme')
	#	encode, decode = self._authentmap[scheme]
	#	return decode(parms)

def _safety_pickle(safe_imports=None, protocol=0):
	import cPickle
	from cPickle import Unpickler, UnpicklingError
	from cStringIO import StringIO
	if not safe_imports:
		find_global = None
	else:
		if protocol >= 2:
			safe_imports = {(it.__module__, it.__name__): it for it in safe_imports}
		else:
			from copy_reg import _reconstructor
			safe_imports = {(ba.__module__, ba.__name__): ba for it in tuple(safe_imports) + (_reconstructor,)
															 for ba in getattr(it, '__mro__', (it,))}
		def find_global(mod, nam):
			try:
				return safe_imports[(mod, nam)]
			except KeyError:
				raise UnpicklingError('Unpickling of %s.%s not allowed' % (mod, nam))
	def _Unpickler(file):
		unpickler = Unpickler(file)
		unpickler.find_global = find_global
		return unpickler
	cPickle.Unpickler = _Unpickler
	def _load(file):
		unpickler = Unpickler(file)
		unpickler.find_global = find_global
		return unpickler.load()
	cPickle.load = _load
	def _loads(val):
		unpickler = Unpickler(StringIO(val))
		unpickler.find_global = find_global
		return unpickler.load()
	cPickle.loads = _loads

def TextMarshaller(charset='utf-8'):
#	uparms = dict(charset=charset)
#	sparms = {}
	return ('text/plain', basestring,
#			lambda obj: isinstance(obj, basestring),
			lambda obj, parms: (obj.encode(charset), parms.update(charset=charset))[0]
									if isinstance(obj, unicode) else obj,
#						(obj, sparms),
			lambda val, parms: val.decode(parms['charset'])
									if parms.get('charset') else val)

def XmlMarshaller(charset=None):
	from xml.etree.cElementTree import Element, tostring, fromstring
#	parms = dict(charset=charset) if charset else {}
	return (('text/xml', 'application/xml'), type(Element(None)),
#			lambda obj: iselement(obj),
			lambda obj, parms: (tostring(obj, charset), charset and parms.update(charset=charset))[0],
			lambda val, parms: fromstring(val))

def PickleMarshaller(safe_imports=None, protocol=0):
	if safe_imports != '*':
		_safety_pickle(safe_imports, protocol)
	from cPickle import dumps, loads
	return ('application/x-python-pickle', object,
#			lambda obj: True,
			lambda obj, parms: (dumps(obj, protocol), protocol and parms.update(protocol=protocol))[0],
			lambda val, parms: loads(val))
#			safe_loads)
#							if type(obj) is VarArgs else
#						(dumps(obj, protocol), oparms),
#			lambda val, parms: loads(val))
#								  if parms.get('varargs') else
#							   loads(val))

def JsonMarshaller(charset=None, indent=None):
	from json import dumps, loads
#	oparms = dict(charset=charset) if charset else {}
#	vparms = dict(varargs=1, **oparms)
	return ('application/json', (tuple, list, dict),
#			lambda obj: isinstance(obj, (tuple, list, dict)),
			lambda obj, parms: (dumps(obj, encoding=charset, indent=indent), charset and parms.update(charset=charset))[0],
#							if type(obj) is VarArgs else oparms),
			lambda val, parms: loads(val, encoding=parms.get('charset')))
#								  if parms.get('varargs') else
#							   loads(val, encoding=parms.get('charset')))

class VarArgs(tuple):
	__slots__ = ()
	def __new__(cls, args, kwargs):
		return tuple.__new__(cls, (args, kwargs))

def VarargsMarshaller(marshaller, *args, **kwargs):
	contype, objtype, _dumps, _loads = marshaller(*args, **kwargs)
	def dumps(obj, parms):
		if type(obj) is not VarArgs:
			return _dumps(obj, parms)
		parms['varargs'] = 1
		if obj[0] or obj[1]:
			return _dumps(tuple(obj), parms)
		else:
			return ''
	def loads(val, parms):
		if not parms.get('varargs'):
			return _loads(val, parms)
		if val:
			return VarArgs(*_loads(val, parms))
		else:
			return VarArgs((), {})
	return contype, objtype, dumps, loads

def XmlRpcEncodeValue(parent, value):
	elt = SubElement(parent, 'value')
	if isinstance(value, basestring):
		SubElement(elt, 'string', text=value)
	elif isinstance(value, bool):
		SubElement(elt, 'boolean', text=str(int(value)))
	elif isinstance(value, int):
		SubElement(elt, 'int', text=str(value))
	elif isinstance(value, float):
		SubElement(elt, 'double', text=str(value))
	elif isinstance(value, datetime):
		SubElement(elt, 'dateTime.iso8601', text=value.replace(microsecond=0).isoformat())
	elif isinstance(value, dict):
		elt = SubElement(elt, 'struct')
		for key, value in value.items():
			member = SubElement(elt, 'member')
			SubElement(member, 'name', text=key)
			XmlRpcEncodeValue(member, value)
	elif isinstance(value, (tuple, list)):
		elt = SubElement(elt, 'array')
		elt = SubElement(elt, 'data')
		for value in value:
			XmlRpcEncodeValue(elt, value)

def XmlRpcEncodeRequest(method, *args):
	root = Element('methodCall')
	SubElement(root, 'methodName', text=method)
	if args:
		params = SubElement(root, 'params')
		for arg in args:
			XmlRpcEncodeValue(SubElement(params, 'param'), arg)
	return root

def XmlRpcEncodeResponse(value, success):
	root = Element('methodResponse')
	if success:
		params = SubElement(root, 'params')
		XmlRpcEncodeValue(SubElement(params, 'param'), value)
	else:
		code, text = value
		fault = SubElement(root, 'fault')
		XmlRpcEncodeValue(fault, {'faultCode':code, 'faultString':text})
	return root

def XmlRpcMarshaller(uri='/RPC2', charset=None):
	def dumps(obj, parms):
		value = XmlRpcEncodeRequest(foo, *foo)
		return _dumps(value, parms)
	def loads(val, parms):
		obj = f
			
def UrlFormMarshaller():
#	from weblib import Form
	return ('application/x-www-form-urlencoded', WebForm,
#			lambda obj: isinstance(obj, Form),
			lambda obj, parms: obj.dumps(),
			lambda val, parms: WebForm.loads(val, parms))


class WebForm(collections.OrderedDict):
	_pctencmap = {ch: ch if ch.isalnum() or ch in "$-_.!*'()," else 
					('%{:02X}'.format(ord(ch)) if ch != ' ' else '+')
					for ch in str(bytearray(range(256)))}
	_pctdecmap = {d1+d2: chr(int(d1+d2, 16)) for d1 in '0123456789ABCDEF'
					for d2 in '0123456789ABCDEF'}
	@staticmethod
	def url_encode(val):
		_pctencfn=WebForm._pctencmap.__getitem__
		return ''.join(map(_pctencfn, val))
	@staticmethod
	def url_decode(val):
		_pctdecfn=WebForm._pctdecmap.__getitem__
		parts = val.replace('+', ' ').split('%')
		return parts[0] + ''.join([_pctdecfn(part[:2].upper()) + part[2:] for part in parts[1:]])
	def itemvals(self):
		for name, value in self.iteritems():
			if value is None:
				continue
			elif isinstance(value, (set, list, tuple)):
				for subval in value:
					yield name, str(subval)
			else:
				yield name, str(value)
	def dumps(self):
		urlenc = self.url_encode
		return '&'.join(['{}={}'.format(urlenc(key), urlenc(val)) for key, val in self.itemvals()])
	@classmethod
	def loads(cls, value):
		self = cls()
		urldec = self.url_decode
		for parm in value.split('&'):
			name, sep, value = parm.partition('=')
			name = urldec(name)
			value = urldec(value)
			prev = self.get(name)
			if prev is None:
				self[name] = value
			elif type(prev) is not list:
				self[name] = [prev, value]
			else:
				prev.append(value)
		return self

#def UrlFormMarshaller(WebForm):
#	def marshal(obj):
#		if not isinstance(obj, HtmlForm):
#			return None, None
#		return obj.encode(MIME_URLFORM), None
#		items = kwargs.items()
#		if not all([isinstance(val, str) for key, val in items]):
#			return None
#		return '&'.join(['{}={}'.format(key, val) for key, val in items]), None
#	def unmarshal(val, args):
#		return WebForm(val, args)
#		obj = {}
#		for parm in val.split('&'):
#			key, sep, val = parm.partition('=')
#			prv = obj.get(key)
#			if prv is None:
#				obj[key] = val
#			elif type(prv) is not list:
#				obj[key] = [prv, val]
#			else:
#				prv.append(val)
#		return ((), obj) if postreq else obj
#	return 'application/x-www-form-urlencoded', encode, decode

def DeflateEncoder():
	from zlib import compress, decompress
	return 'deflate', compress, decompress

def Bzip2Encoder():
	from bz2 import compress, decompress
	return 'bzip2', compress, decompress

def GzipEncoder():
	from gzip import GzipFile
	from io import BytesIO
	def compress(val):
		buf = BytesIO()
		with GzipFile(fileobj=buf, mode='wb') as gzf:
			gzf.write(val)
		return buf.getvalue()
	def decompress(val):
		buf = BytesIO(val)
		with GzipFile(fileobj=buf, mode='rb') as gzf:
			val = gzf.read()
		return val
	return 'gzip', compress, decompress

def BasicAuthent():
	from binascii import a2b_base64, b2a_base64
	def encode(parms):
		return b2a_base64('{userid}:{passwd}'.format(**parms))[:-1]
	def decode(parms):
		userid, sep, passwd = a2b_base64(parms).partition(':')
		return {'userid':userid, 'passwd':passwd}
	return 'basic', encode, decode

class BaseTransport(object):

	def __enter__(self):
		return self

	def __exit__(self, *exc_info):
		self.close()

	def close(self):
		if self.ios is not None:
			self.ios.close()
			self.ios = None

	def _write_request(self, method, resource, version):
		line = '{} {} HTTP/{:.2}\r\n'.format(method, resource, version)
		self.ios.write(line)

	def _read_request(self):
		line = self.ios.readline()
		if not line:
			raise HttpError(400, 'EOF reading request')
		try:
			method, resource, version = line.split(None, 2)
			version = float(version.split('HTTP/')[-1])
		except ValueError:
			raise HttpError(400, 'Invalid request line')
		else:
			return method, resource, version

	def _write_response(self, status, reason, version):
		line = 'HTTP/{:.2} {:03} {}\r\n'.format(version, status, reason)
		self.ios.write(line)

	def _read_response(self):
		line = self.ios.readline()
		if not line:
			raise HttpError(400, 'EOF reading response')
		try:
			version, status, reason = line.split(None, 2)
			version = float(version.split('HTTP/')[-1])
			status = int(status)
		except ValueError:
			raise HttpError(400, 'Invalid status line')
		else:
			return status, reason, version

	def _write_message(self, headers, body, eof):
		for key, val in headers.iteritems():
			self.ios.write('{}: {}\r\n'.format(key, val))
		self.ios.write('\r\n')
		if body:
			self.ios.write(body)
		self.ios.flush()
		if eof:
			self.ios.raw.shutdown(socketio.SHUT_WR)

	def _read_headers(self, headers):
		try:
			for line in self.ios:
				if line.startswith(('\r', '\n')):
					break
				if line[0].isspace():
					headers[key] += line.rstrip()
				else:
					key, val = line.split(':', 1)
					key = key.title()
					val = val.strip()
					if key in headers:
						headers[key] += ', ' + val
					else:
						headers[key] = val
			else:
				raise HttpError(400, 'EOF reading headers')
		except ValueError, NameError:
			raise HttpError(400, 'Invalid header line')

	def _read_message(self, bodyflag=None):
		headers = {}
		self._read_headers(headers)
		trnsfrenc = headers.get(_TRANSFER_ENCODING)
		conlength = headers.get(_CONTENT_LENGTH)
		if bodyflag is False:
			body = None
		elif trnsfrenc == 'chunked':
			chunks = []
			while True:
				line = self.ios.readline()
				if not line:
					raise HttpError(400, 'EOF reading chunk')
				size = int(line.split(';', 1)[0])
				if not size:
					break
				chunks.append(self.ios.read(size))
				self.ios.readline()
			body = ''.join(chunks)
			self._read_headers(headers)
		elif conlength is not None:
			try:
				size = int(conlength)
			except ValueError:
				raise HttpError(400, 'Invalid %s' % _CONTENT_LENGTH)
			body = self.ios.read(size)
			if len(body) != size:
				raise HttpError(400, 'EOF reading content')
		elif bodyflag is True:
			# The HTTP 1.1 spec says that response messages can have a body
			# even if Content-Length or Transfer-Encoding is not specified,
			# so we should read until the server closes the connection. This
			# seems kind of sketchy when using persistent connections, so we
			# might want to raise an error if the server hasn't said it is 
			# going to close the connection.
			#if headers.get(_CONNECTION) != 'close':
			#	raise HttpError(411, '%s is required' % _CONTENT_LENGTH)
			body = self.ios.read()
		else:
			# Request messages are easier, they only have bodies when indicated
			# by Content-Length or Transfer-Encoding.
			body = None
		
		return headers, body
		
class ClientTransport(BaseTransport):

	def __init__(self, addr, timeout):
		self.ios = None
		self._addr = addr
		self._timeout = timeout

	def send_request(self, request, more=False):
		if self.ios is not None:
			rios, wios, xios = select((self.ios,), (), (), 0)
			if rios and not self.ios.raw.recv(1, socketio.MSG_PEEK):
				self.ios.close()
		if self.ios is None or self.ios.closed:
			self.ios = socketio.open(self._addr, 'rwbs', timeout=self._timeout)
		method, resource, version, headers, body = request
		self._method = method
		self._more = more
		headers[_HOST] = self._addr
		headers[_USER_AGENT] = AGENT_NAME
		headers[_DATE] = request.format_date(datetime.utcnow())
		if not more:
			headers[_CONNECTION] = 'close'
		self._write_request(method, resource, version)
		self._write_message(headers, body, not more)

	def recv_response(self):
#		try:
			status, reason, version = self._read_response()
			bodyflag = self._method != 'HEAD' and status >= 200 and status not in (204, 205, 304)
			headers, body = self._read_message(bodyflag)
			if not (self._more and version >= 1.1 
							   and headers.get(_CONNECTION) != 'close'):
				self.ios.close()
			return Response(status, reason, version, headers, body)
#		except socketio.BlockingIOError:
#			response = Response(408, "Request timed out")
#		except HttpError as error:
#			response = Response(error.status, error.reason)
#		self.ios.close()
#		return response

class ServerTransport(BaseTransport):

	def __init__(self, ios):
		self.ios = ios
		self._more = True

	def more(self):
		if self._more:
			rios, wios, xios = select((self.ios,), (), (), self.ios.timeout)
			if not rios or not self.ios.raw.recv(1, socketio.MSG_PEEK):
				self._more = False
		return self._more

	def recv_request(self, more=False):
#		try:
			method, resource, version = self._read_request()
			headers, body = self._read_message()
			self._more = (more and version >= 1.1 
							   and headers.get(_CONNECTION) != 'close')
			return Request(method, resource, version, headers, body)
#		except socketio.BlockingIOError:
#			self._more = False
#			raise HttpError(408, "Request timed out")
#			self.error_response = Response(408, "Request timed out")
#		except:
#			self._more = False
#			raise
#		except HttpError as error:
#			if error.status == 499:  # EOF indicator, the client has closed the connection
#				self.error_response = None
#			else:
#				self.error_response = Response(error.status, error.reason)
#		self._more = False
#		return None

	def send_response(self, response):
#		if response.reason == 0:
#			return
		status, reason, version, headers, body = response
		headers[_SERVER] = AGENT_NAME
		headers[_DATE] = response.format_date(datetime.utcnow())
		if not self._more:
			headers[_CONNECTION] = 'close'
		self._write_response(status, reason, version)
		self._write_message(headers, body, not self._more)

if 1:
	manager = ContentManager([TextMarshaller(), XmlMarshaller(), PickleMarshaller()])

	def test_cli(addr, timeout, *args, **kwargs):
		with ClientTransport(addr, timeout) as transport:
			print "=============================="
			request = manager.encode_message(Request, 'GET', '/test/path')
			print 'Sending:\n', request
			print "=============================="
			transport.send_request(request, True)
			response = transport.recv_response()
			print 'Received:\n', response
			print '  decoded:', manager.decode_message(response)
			print "=============================="
			request =  manager.encode_message(Request, 'POST', '/test/otherpath', (args, kwargs))
			print 'Sending:\n', request
			print "=============================="
			transport.send_request(request)
			response = transport.recv_response()
			print 'Received:\n', response
			print '  decoded:', manager.decode_message(response)
			print "=============================="
	
	def test_srv(addr, timeout, obj):
#		contype = 'text/plain' if type(obj) is str else 'application/x-python-pickle'
		listenios = socketio.open(addr, 'rwbs@', timeout=timeout, backlog=1)
		try:
			serverios = listenios.accept()
			with ServerTransport(serverios) as transport:
				while transport.more:
					print "=============================="
					request = transport.recv_request(True)
					print 'Received:\n', request
					print '  decoded:', manager.decode_message(request)
					print "=============================="
					response = manager.encode_message(Response, 200, 'test ok', obj)
					print 'Sending:\n', response
					transport.send_response(response)
					print "=============================="
		finally:
			listenios.close()
		
